package com.msi;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.simpleframework.xml.ElementMap;
import org.simpleframework.xml.Root;

@Root
public class Graph {

	protected static final Logger log = Logger.getLogger(Graph.class);

	/**
	 * all vertices in graph key: vertex ID value: vertex
	 */
	@ElementMap(entry = "vertex", key = "vertexId", attribute = false, inline = true)
	Map<Integer, Vertex> vertices = new HashMap<Integer, Vertex>();

	/**
	 * true if graph doesn't have coordinates
	 */
	boolean hasCoordinates = false;

	/**
	 * map vertexId to coordinates used in graph visualization
	 */
	Map<Integer, Point2D> coordinateMap;

	private Graph() {
		boolean rootIsConfigured = Logger.getRootLogger().getAllAppenders()
				.hasMoreElements();
		if (!rootIsConfigured)
			BasicConfigurator.configure();
	}

	/**
	 * @param matrix
	 *            graph structure as matrix
	 * @return
	 */
	public static Graph createGraphByNeighbourMatrix(List<List<Integer>> matrix) {
		long start = System.currentTimeMillis();
		Graph graph = new Graph();
		int vertexCount = matrix.size();

		graph.initializeVertices(vertexCount);
		graph.createEdgesByNeighbourhoodMatrix(matrix, vertexCount);

		long stop = System.currentTimeMillis();
		log.info("Tworzenie grafu z macierzy zajelo:" + (stop - start));
		graph.printOnConsole();
		return graph;
	}

	/**
	 * @param numberOfVertices
	 *            how many vertices will be created
	 * @param density
	 *            Probability of creating single edge between two vertices
	 */
	public Graph(int numberOfVertices, double density) {
		Random rand = new Random();
		/** generate vertices */
		for (int i = 0; i < numberOfVertices; ++i) {
			vertices.put(i, new Vertex(i));
		}

		/** create connections */
		for (int i = 0; i < numberOfVertices; ++i) {
			for (int j = i + 1; j < numberOfVertices; ++j) {
				if (rand.nextDouble() < density)
					vertices.get(i).addNeighbour(vertices.get(j));
			}
		}
	}

	private void createEdgesByNeighbourhoodMatrix(List<List<Integer>> matrix,
			int vertexCount) {
		for (int i = 0; i < vertexCount; ++i)
			for (int j = i + 1; j < vertexCount; ++j)
				addEdgeIfExistsInMatrix(i, j, matrix);
	}

	private void addEdgeIfExistsInMatrix(int currentVertex, int neighbour,
			List<List<Integer>> matrix) {
		if (matrix.get(currentVertex).get(neighbour) == 1) {
			vertices.get(currentVertex).addNeighbour(vertices.get(neighbour));
		}
	}

	private void initializeVertices(int vertexCount) {
		for (int i = 0; i < vertexCount; ++i)
			vertices.put(i, new Vertex(i));
	}

	public void printOnConsole() {

		List<Vertex> verticesList = createSortedVericesList();
		log.info("rozpoczynamy rysowanie grafu w postaci list sasiedztwa");
		for (Vertex v : verticesList) {
			v.print();
			System.out.println("");
		}
	}

	private List<Vertex> createSortedVericesList() {
		List<Vertex> verticesList = getVerticesList();
		sortVerticesById(verticesList);
		return verticesList;
	}

	private List<Vertex> getVerticesList() {
		List<Vertex> verticesList = new ArrayList<Vertex>();

		Set<Entry<Integer, Vertex>> set = vertices.entrySet();
		for (Entry<Integer, Vertex> e : set) {
			verticesList.add(e.getValue());
		}
		return verticesList;
	}

	private void sortVerticesById(List<Vertex> verticesList) {
		Collections.sort(verticesList, new Comparator<Vertex>() {

			@Override
			public int compare(Vertex o1, Vertex o2) {
				int id1 = o1.getId();
				int id2 = o2.getId();
				if (id1 < id2)
					return -1;
				if (id1 > id2)
					return 1;
				return 0;
			}

		});
	}

	private void sortVerticesByDegree(List<com.msi.Vertex> verticesList) {
		Collections.sort(verticesList, new Comparator<Vertex>() {
			@Override
			public int compare(Vertex o1, Vertex o2) {
				int d1 = o1.getDegree();
				int d2 = o2.getDegree();
				if (d1 > d2)
					return -1;
				if (d1 < d2)
					return 1;
				return 0;
			}
		});
	}

	public Vertex getVertex(Integer id) {
		return vertices.get(id);
	}

	public int getSize() {
		return vertices.size();
	}

	public int getNumberOfColors() {
		return 0;
	}

	public Map<Integer, Vertex> getVertices() {
		return vertices;
	}

	public void setVertices(Map<Integer, Vertex> vertices) {
		this.vertices = vertices;
	}

	public void setCoordinates(Map<Integer, Point2D> map) {

		if (map == null)
			return;
		coordinateMap = map;
		/*
		 * Set<Entry<Integer, Point2D>> set = map.entrySet(); for
		 * (Entry<Integer, Point2D> e : set) { Vertex v =
		 * vertices.get(e.getKey()); v.setX(e.getValue().getX());
		 * v.setY(e.getValue().getY()); }
		 */
		hasCoordinates = true;
	}

	public void resetColors() {
		for (Vertex v : vertices.values()) {
			v.resetColor();
		}
	}

	public int getNumberOfEdges() {
		int result = 0;
		for (Vertex v : vertices.values()) {
			result += v.getDegree();
		}
		return result / 2;
	}

	public int getNumberOfVertices() {
		return vertices.values().size();
	}

	public boolean hasCoordinates() {
		return hasCoordinates;
	}

	public Map<Integer, Point2D> getCoordinateMap() {
		return coordinateMap;
	}

	/**
	 * Zwraca głęboką kopię grafu
	 * 
	 * @return głęboka kopia grafu
	 */
	public Graph getDeepCopy() {
		// TODO poprawna obsługa współrzędnych (kopia coordinateMap)
		Graph g = new Graph();

		// dodanie samych wierzchołków bez krawędzi
		for (Vertex v : vertices.values()) {
			g.addVertex(v.createVertexCopyWithoutNeighbors());
		}

		// dodanie krawędzi
		for (Vertex v : vertices.values()) {
			for (Vertex neighbor : v.getNeighborhood()) {
				g.getVertex(v.getId()).addNeighbourOneWay(
						g.getVertex(neighbor.getId()));
			}
		}
		return g;
	}

	public void addVertex(Vertex v) {
		if (!vertices.containsKey(v.getId())) {
			vertices.put(v.getId(), v);
		}
	}

	/**
	 * Zwraca listę nieodwiedzonych jeszcze sąsiadów posortowaną po ich
	 * stopniach
	 * 
	 * @param asc
	 *            czy sortowanie ma być rosnące
	 * @return lista posortowanych sąsiadów
	 */
	public List<Vertex> getNotVisitedVerticesSortedByDegree(boolean asc) {
		List<Vertex> result = new ArrayList<Vertex>();
		for (Vertex v : vertices.values()) {
			if (!v.isVisited())
				result.add(v);
		}

		if (asc) {
			Collections.sort(result, new Comparator<Vertex>() {
				@Override
				public int compare(Vertex o1, Vertex o2) {
					int d1 = o1.getDegree();
					int d2 = o2.getDegree();
					if (d1 < d2)
						return -1;
					if (d1 > d2)
						return 1;
					return 0;
				}
			});
		} else {
			sortVerticesByDegree(result);
		}

		return result;
	}

	public List<Vertex> getVerticesSortedByDegree() {
		List<Vertex> result = new ArrayList<Vertex>();
		for (Vertex v : vertices.values()) {
			result.add(v);
		}
		sortVerticesByDegree(result);
		return result;
	}
}
